page.tsx 33 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706
  1. "use client";
  2. import { useTranslations } from "next-intl";
  3. import { Link } from "@/i18n/navigation";
  4. import { useAuth } from "@/providers/auth-provider";
  5. import { useEffect, useState } from "react";
  6. import { Wallet, BookOpen, FileText, History, LogOut, ChevronRight, Settings, UserRoundPen } from "lucide-react";
  7. import { fetchWalletBalance } from "@/lib/account-api";
  8. import {
  9. fetchBalanceRecordList,
  10. type BalanceRecord,
  11. } from "@/lib/balance-record-api";
  12. import { fetchCustomUserInfo, tryFetchCustomUserInfo, updateUserInfo } from "@/lib/user-info-api";
  13. import {
  14. cancelOrder,
  15. fetchOrderList,
  16. getOrderStatusLabel,
  17. type OrderRecord,
  18. } from "@/lib/order-api";
  19. import {
  20. fetchPurchasedCourses,
  21. type PurchasedCourse,
  22. } from "@/lib/goods-order-api";
  23. import {
  24. fetchWithdrawalList,
  25. getWithdrawalStatusLabel,
  26. type WithdrawalRecord,
  27. } from "@/lib/withdrawal-api";
  28. import { cn } from "@/lib/utils";
  29. import { ReferralCodeBadge } from "@/components/referral-code-badge";
  30. import { AccountLoginRequired } from "@/components/auth/account-login-required";
  31. export default function AccountPage() {
  32. const t = useTranslations("account");
  33. const { user, isReady, updateProfile, logout } = useAuth();
  34. // 核心状态
  35. const [walletBalance, setWalletBalance] = useState<number | null>(null);
  36. const [walletLoading, setWalletLoading] = useState(false);
  37. const [balanceDetailOpen, setBalanceDetailOpen] = useState(false);
  38. const [balanceDetailLoading, setBalanceDetailLoading] = useState(false);
  39. const [balanceDetailError, setBalanceDetailError] = useState<string | null>(
  40. null,
  41. );
  42. const [balanceRecords, setBalanceRecords] = useState<BalanceRecord[]>([]);
  43. const [purchasedCourses, setPurchasedCourses] = useState<PurchasedCourse[]>([]);
  44. const [orders, setOrders] = useState<OrderRecord[]>([]);
  45. const [ordersLoading, setOrdersLoading] = useState(false);
  46. const [withdrawals, setWithdrawals] = useState<WithdrawalRecord[]>([]);
  47. const [withdrawalsLoading, setWithdrawalsLoading] = useState(false);
  48. const [cancelTarget, setCancelTarget] = useState<OrderRecord | null>(null);
  49. const [cancelLoading, setCancelLoading] = useState(false);
  50. const [editInfoOpen, setEditInfoOpen] = useState(false);
  51. const [editName, setEditName] = useState("");
  52. const [editPhone, setEditPhone] = useState("");
  53. const [editIdentity, setEditIdentity] = useState("");
  54. const [editSubmitting, setEditSubmitting] = useState(false);
  55. const [editError, setEditError] = useState<string | null>(null);
  56. const [logoutConfirmOpen, setLogoutConfirmOpen] = useState(false);
  57. const [referralCode, setReferralCode] = useState<string | null>(null);
  58. useEffect(() => {
  59. if (!user) return;
  60. let cancelled = false;
  61. async function loadData() {
  62. setWalletLoading(true);
  63. setOrdersLoading(true);
  64. setWithdrawalsLoading(true);
  65. try {
  66. const [balanceResult, orderResult, courseResult, withdrawResult] =
  67. await Promise.allSettled([
  68. fetchWalletBalance(),
  69. fetchOrderList({ current: 1, row: 10 }),
  70. fetchPurchasedCourses(),
  71. fetchWithdrawalList(),
  72. ]);
  73. if (cancelled) return;
  74. if (balanceResult.status === "fulfilled") {
  75. setWalletBalance(balanceResult.value);
  76. }
  77. if (orderResult.status === "fulfilled") {
  78. setOrders(orderResult.value.list);
  79. }
  80. if (courseResult.status === "fulfilled") {
  81. setPurchasedCourses(courseResult.value);
  82. } else {
  83. console.error(courseResult.reason);
  84. }
  85. if (withdrawResult.status === "fulfilled") {
  86. setWithdrawals(withdrawResult.value.list);
  87. }
  88. } catch (e) {
  89. console.error(e);
  90. } finally {
  91. if (!cancelled) {
  92. setWalletLoading(false);
  93. setOrdersLoading(false);
  94. setWithdrawalsLoading(false);
  95. }
  96. }
  97. }
  98. loadData();
  99. return () => { cancelled = true; };
  100. }, [user]);
  101. useEffect(() => {
  102. if (!user?.email) return;
  103. let cancelled = false;
  104. void tryFetchCustomUserInfo().then((info) => {
  105. if (cancelled || !info) return;
  106. if (info.referralCode) setReferralCode(info.referralCode);
  107. if (info.levelLabel && !user.levelLabel) {
  108. updateProfile({ levelLabel: info.levelLabel });
  109. }
  110. });
  111. return () => { cancelled = true; };
  112. // 仅登录用户变化时拉取,避免 updateProfile 触发重复请求
  113. // eslint-disable-next-line react-hooks/exhaustive-deps
  114. }, [user?.email, user?.levelLabel]);
  115. async function openBalanceDetail() {
  116. setBalanceDetailOpen(true);
  117. setBalanceDetailLoading(true);
  118. setBalanceDetailError(null);
  119. try {
  120. const list = await fetchBalanceRecordList({ current: 1, row: 50 });
  121. setBalanceRecords(list);
  122. } catch (e) {
  123. setBalanceRecords([]);
  124. setBalanceDetailError((e as Error)?.message || "消费明细加载失败,请稍后重试");
  125. } finally {
  126. setBalanceDetailLoading(false);
  127. }
  128. }
  129. function balanceTypeLabel(type: BalanceRecord["type"]) {
  130. return type === 2 ? "奖励" : "取款";
  131. }
  132. function openEditInfo() {
  133. if (!user) return;
  134. setEditError(null);
  135. setEditName(user.name ?? "");
  136. setEditPhone(user.phone ?? "");
  137. setEditIdentity(user.identity ?? "");
  138. setEditInfoOpen(true);
  139. }
  140. function closeEditInfo() {
  141. setEditInfoOpen(false);
  142. setEditSubmitting(false);
  143. setEditError(null);
  144. }
  145. async function submitEditInfo() {
  146. const name = editName.trim();
  147. const phone = editPhone.trim();
  148. const identity = editIdentity.trim();
  149. if (!name) {
  150. setEditError("请输入姓名");
  151. return;
  152. }
  153. setEditSubmitting(true);
  154. setEditError(null);
  155. try {
  156. await updateUserInfo({ name, phone, identity });
  157. const info = await fetchCustomUserInfo();
  158. updateProfile({
  159. name: info.name || name,
  160. phone: info.phone || phone,
  161. identity: info.identity || identity,
  162. });
  163. closeEditInfo();
  164. if (typeof window !== "undefined") window.location.reload();
  165. } catch (e) {
  166. setEditError((e as Error)?.message || "修改失败,请稍后重试");
  167. } finally {
  168. setEditSubmitting(false);
  169. }
  170. }
  171. async function handleConfirmCancel() {
  172. if (!cancelTarget) return;
  173. try {
  174. setCancelLoading(true);
  175. await cancelOrder(cancelTarget.id);
  176. const res = await fetchOrderList({ current: 1, row: 10 });
  177. setOrders(res.list);
  178. setCancelTarget(null);
  179. } catch {
  180. alert("取消失败,请重试");
  181. } finally {
  182. setCancelLoading(false);
  183. }
  184. }
  185. // 高级状态色系:针对黑底优化
  186. function getStatusStyle(status: OrderRecord["status"] | WithdrawalRecord["status"]) {
  187. const s = String(status).trim();
  188. const sLower = s.toLowerCase();
  189. // 已完成:绿色
  190. if (s === "2" || sLower === "completed" || sLower === "success") {
  191. return "text-emerald-400 border-emerald-400/30 bg-emerald-400/10";
  192. }
  193. // 已拒绝/已失败:红色(含:3/4/5 以及 failed/rejected/cancelled 字符串)
  194. if (
  195. s === "3" || // 已拒绝 / 支付失败(支付失败在 order-api 中为 3)
  196. s === "4" ||
  197. s === "5" ||
  198. sLower === "failed" ||
  199. sLower === "rejected" ||
  200. sLower === "cancelled"
  201. ) {
  202. return "text-rose-400 border-rose-400/30 bg-rose-400/10";
  203. }
  204. return "text-amber-400 border-amber-400/30 bg-amber-400/10";
  205. }
  206. if (!isReady) return <div className="min-h-screen bg-[#050b14] flex items-center justify-center text-slate-500">加载中...</div>;
  207. if (!user) {
  208. return <AccountLoginRequired />;
  209. }
  210. return (
  211. <div className="min-h-screen bg-[#050b14] pb-24 text-slate-300 font-sans">
  212. {/* 全局背景装饰 */}
  213. <div className="pointer-events-none fixed inset-0 z-0">
  214. <div className="absolute left-1/4 top-0 h-[500px] w-[500px] rounded-full bg-blue-900/10 blur-[120px]" />
  215. <div className="absolute right-1/4 bottom-0 h-[500px] w-[500px] rounded-full bg-[#b89458]/5 blur-[120px]" />
  216. </div>
  217. <div className="relative z-10 site-container pt-12">
  218. {/* 1. 欢迎区 & 统计卡片 (全玻璃质感) */}
  219. <section className="ui-enter mb-10 rounded-[2.5rem] border border-white/10 bg-white/5 p-8 backdrop-blur-2xl shadow-2xl">
  220. <div className="flex flex-col gap-8 md:flex-row md:items-center md:justify-between">
  221. <div>
  222. <p className="text-xs font-bold tracking-[0.3em] text-[#b89458] mb-2 uppercase">User Control Center</p>
  223. <div className="flex flex-wrap items-center gap-x-4 gap-y-2">
  224. <h1 className="font-serif text-3xl font-bold text-white md:text-5xl tracking-wide">
  225. {t("welcome", { name: user.name })}
  226. </h1>
  227. {referralCode ? (
  228. <ReferralCodeBadge code={referralCode} />
  229. ) : null}
  230. </div>
  231. <p className="mt-3 text-sm text-slate-400">欢迎回来,在这里掌控您的个人资产与学习进程。</p>
  232. </div>
  233. <Link
  234. href="/account/withdraw-apply"
  235. className="ui-interactive-btn flex items-center gap-2 rounded-2xl bg-gradient-to-br from-[#f3deae] to-[#d9be88] px-8 py-4 text-sm font-bold text-[#5c461a] shadow-xl shadow-[#b89458]/10"
  236. >
  237. <Wallet size={18} /> 发起领取申请
  238. </Link>
  239. </div>
  240. <div className="mt-12 grid grid-cols-2 gap-4 md:grid-cols-4 lg:gap-6">
  241. <button
  242. type="button"
  243. onClick={openBalanceDetail}
  244. className="group rounded-2xl border border-white/5 bg-white/5 p-6 text-center transition-all hover:bg-white/10 focus:outline-none focus:ring-2 focus:ring-[#b89458]/40"
  245. >
  246. <div className="flex flex-col items-center gap-3 mb-4">
  247. <div
  248. className={cn(
  249. "p-2 rounded-xl bg-white/5 group-hover:scale-110 transition-transform text-[#f3deae]",
  250. )}
  251. >
  252. <Wallet size={18} />
  253. </div>
  254. <span className="text-[11px] font-bold uppercase tracking-widest text-slate-500">
  255. 钱包余额
  256. </span>
  257. </div>
  258. <p className="text-3xl font-bold tracking-tight text-white">
  259. {walletLoading ? "..." : `$${(walletBalance ?? 0).toFixed(2)}`}
  260. </p>
  261. <p className="mt-2 text-[11px] font-semibold text-slate-500">
  262. 点击查看消费明细
  263. </p>
  264. </button>
  265. {[
  266. { label: "已购课程", value: purchasedCourses.length, icon: BookOpen, color: "text-sky-400" },
  267. { label: "订单总数", value: orders.length, icon: FileText, color: "text-emerald-400" },
  268. { label: "领取记录", value: withdrawals.length, icon: History, color: "text-teal-400" }
  269. ].map((stat, i) => (
  270. <div
  271. key={i}
  272. className="group rounded-2xl border border-white/5 bg-white/5 p-6 text-center transition-all hover:bg-white/10"
  273. >
  274. <div className="flex flex-col items-center gap-3 mb-4">
  275. <div className={cn("p-2 rounded-xl bg-white/5 group-hover:scale-110 transition-transform", stat.color)}>
  276. <stat.icon size={18} />
  277. </div>
  278. <span className="text-[11px] font-bold uppercase tracking-widest text-slate-500">{stat.label}</span>
  279. </div>
  280. <p className="text-3xl font-bold tracking-tight text-white">{stat.value}</p>
  281. </div>
  282. ))}
  283. </div>
  284. </section>
  285. <div className="grid gap-8 lg:grid-cols-[300px_minmax(0,1fr)]">
  286. {/* 2. 侧边导航 */}
  287. <aside className="ui-enter flex flex-col gap-6">
  288. <div className="rounded-[2.5rem] border border-white/10 bg-white/5 p-6 backdrop-blur-2xl shadow-xl">
  289. <div className="flex items-center gap-4 mb-10 p-2">
  290. <div className="flex h-14 w-14 shrink-0 items-center justify-center rounded-[1.25rem] bg-gradient-to-br from-slate-800 to-slate-900 text-xl font-bold text-[#f3deae] shadow-lg ring-1 ring-white/10">
  291. {(user.name || "U").slice(0, 1).toUpperCase()}
  292. </div>
  293. <div className="min-w-0">
  294. <p className="truncate text-lg font-bold text-white">{user.name}</p>
  295. <p className="text-xs font-medium text-slate-500 tracking-wide uppercase">Member Account</p>
  296. </div>
  297. </div>
  298. <nav className="flex flex-col gap-2">
  299. {[
  300. { label: "我的课程", href: "/account/purchased-courses", icon: BookOpen },
  301. { label: "订单管理", href: "/account/orders", icon: FileText },
  302. { label: "领取记录", href: "/account/withdrawals", icon: History },
  303. { label: "修改信息", href: "#edit-info", icon: UserRoundPen, onClick: openEditInfo },
  304. { label: "修改密码", href: "/account/change-password", icon: Settings }
  305. ].map((item) =>
  306. "onClick" in item ? (
  307. <button
  308. key={item.href}
  309. type="button"
  310. onClick={item.onClick}
  311. className="group flex w-full items-center justify-between rounded-2xl px-5 py-4 text-sm font-semibold text-slate-400 transition-all hover:bg-white/10 hover:text-white"
  312. >
  313. <span className="flex items-center gap-4">
  314. <item.icon
  315. size={20}
  316. className="text-slate-500 group-hover:text-[#f3deae]"
  317. />{" "}
  318. {item.label}
  319. </span>
  320. <ChevronRight
  321. size={16}
  322. className="opacity-0 -translate-x-2 transition-all group-hover:opacity-100 group-hover:translate-x-0"
  323. />
  324. </button>
  325. ) : (
  326. <Link
  327. key={item.href}
  328. href={item.href}
  329. className="group flex items-center justify-between rounded-2xl px-5 py-4 text-sm font-semibold text-slate-400 transition-all hover:bg-white/10 hover:text-white"
  330. >
  331. <span className="flex items-center gap-4">
  332. <item.icon
  333. size={20}
  334. className="text-slate-500 group-hover:text-[#f3deae]"
  335. />{" "}
  336. {item.label}
  337. </span>
  338. <ChevronRight
  339. size={16}
  340. className="opacity-0 -translate-x-2 transition-all group-hover:opacity-100 group-hover:translate-x-0"
  341. />
  342. </Link>
  343. ),
  344. )}
  345. </nav>
  346. <div className="mt-10 border-t border-white/5 pt-6">
  347. <button
  348. type="button"
  349. onClick={() => setLogoutConfirmOpen(true)}
  350. className="flex w-full items-center gap-4 rounded-2xl px-5 py-4 text-sm font-bold text-rose-400 transition-all hover:bg-rose-500/10"
  351. >
  352. <LogOut size={20} /> 退出登录
  353. </button>
  354. </div>
  355. </div>
  356. </aside>
  357. {/* 3. 主内容区 */}
  358. <div className="flex flex-col gap-8">
  359. {/* 订单记录卡片 */}
  360. <section className="ui-enter ui-enter-delay-1 rounded-[2.5rem] border border-white/10 bg-white/5 p-8 backdrop-blur-2xl shadow-xl">
  361. <div className="flex items-center justify-between mb-8">
  362. <h2 className="font-serif text-2xl font-bold text-white">最新订单记录</h2>
  363. <Link href="/account/orders" className="text-xs font-bold text-[#b89458] hover:text-[#f3deae] transition-colors">查看全部</Link>
  364. </div>
  365. {ordersLoading ? (
  366. <div className="py-12 flex justify-center text-slate-500">数据同步中...</div>
  367. ) : orders.length === 0 ? (
  368. <div className="py-12 flex flex-col items-center justify-center rounded-3xl border border-dashed border-white/10 bg-white/5 text-slate-500">
  369. <p className="text-sm">{t("noOrders")}</p>
  370. </div>
  371. ) : (
  372. <div className="space-y-4">
  373. {orders.slice(0, 2).map((o) => (
  374. <div key={o.serial} className="group rounded-[1.5rem] border border-white/5 bg-white/5 p-5 transition-all hover:bg-white/10">
  375. <div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
  376. <div className="flex items-center gap-5">
  377. <div className="flex h-12 w-12 shrink-0 items-center justify-center rounded-2xl bg-white/5 text-[#f3deae]">
  378. <FileText size={22} />
  379. </div>
  380. <div>
  381. <p className="text-[15px] font-bold text-white group-hover:text-[#f3deae] transition-colors">{o.details}</p>
  382. <p className="text-xs font-medium text-slate-500 mt-1">NO: {o.serial}</p>
  383. </div>
  384. </div>
  385. <div className="flex shrink-0 items-center justify-end gap-4 sm:gap-6">
  386. <div className="flex flex-col items-end gap-1.5">
  387. <p className="text-xl font-bold text-white tracking-tight">${o.amount}</p>
  388. <span className={cn("rounded-full border px-3 py-1 text-[10px] font-bold", getStatusStyle(o.status))}>
  389. {getOrderStatusLabel(o.status)}
  390. </span>
  391. </div>
  392. {o.status === 1 && (
  393. <button onClick={() => setCancelTarget(o)} className="text-xs font-bold text-rose-400 hover:text-rose-300">取消</button>
  394. )}
  395. </div>
  396. </div>
  397. </div>
  398. ))}
  399. </div>
  400. )}
  401. </section>
  402. {/* 领取记录卡片 (还原显示) */}
  403. <section className="ui-enter ui-enter-delay-2 rounded-[2.5rem] border border-white/10 bg-white/5 p-8 backdrop-blur-2xl shadow-xl">
  404. <div className="flex items-center justify-between mb-8">
  405. <h2 className="font-serif text-2xl font-bold text-white">奖学金领取记录</h2>
  406. <Link href="/account/withdrawals" className="text-xs font-bold text-[#b89458] hover:text-[#f3deae] transition-colors">查看全部记录</Link>
  407. </div>
  408. {withdrawalsLoading ? (
  409. <div className="py-12 flex justify-center text-slate-500">记录检索中...</div>
  410. ) : withdrawals.length === 0 ? (
  411. <div className="py-12 flex flex-col items-center justify-center rounded-3xl border border-dashed border-white/10 bg-white/5 text-slate-500">
  412. <p className="text-sm">暂无领取记录</p>
  413. </div>
  414. ) : (
  415. <div className="space-y-4">
  416. {withdrawals.slice(0, 2).map((w) => (
  417. <div key={w.serial} className="group rounded-[1.5rem] border border-white/5 bg-white/5 p-5 transition-all hover:bg-white/10">
  418. <div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
  419. <div className="flex items-center gap-5">
  420. <div className="flex h-12 w-12 shrink-0 items-center justify-center rounded-2xl bg-white/5 text-teal-400">
  421. <History size={22} />
  422. </div>
  423. <div>
  424. <p className="text-[15px] font-bold text-white group-hover:text-teal-400 transition-colors">{w.details}</p>
  425. <p className="text-xs font-medium text-slate-500 mt-1">流水号: {w.serial}</p>
  426. </div>
  427. </div>
  428. <div className="flex shrink-0 flex-col items-end gap-1.5">
  429. <p className="text-xl font-bold text-white tracking-tight">${w.amount}</p>
  430. <span className={cn("rounded-full border px-3 py-1 text-[10px] font-bold", getStatusStyle(w.status))}>
  431. {getWithdrawalStatusLabel(w.status)}
  432. </span>
  433. </div>
  434. </div>
  435. </div>
  436. ))}
  437. </div>
  438. )}
  439. </section>
  440. {/* 已购买课程 */}
  441. <section className="ui-enter ui-enter-delay-3 rounded-[2.5rem] border border-white/10 bg-white/5 p-8 backdrop-blur-2xl shadow-xl">
  442. <div className="flex items-center justify-between mb-8">
  443. <h2 className="font-serif text-2xl font-bold text-white">已购课程</h2>
  444. <Link href="/account/purchased-courses" className="text-xs font-bold text-[#b89458] hover:text-[#f3deae] transition-colors">浏览全部课程</Link>
  445. </div>
  446. <div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
  447. {purchasedCourses.length > 0 ? (
  448. purchasedCourses.slice(0, 3).map((c) => (
  449. <Link key={c.id} href={`/courses/${c.goodsId}${c.goodsType === 5 ? '?cat=strategy' : ''}`} className="group relative flex flex-col overflow-hidden rounded-[2rem] border border-white/5 bg-white/5 transition-all hover:-translate-y-1 hover:bg-white/10">
  450. <div className="relative h-40 w-full overflow-hidden">
  451. {c.coverUrl ? (
  452. <img src={c.coverUrl} alt={c.title} className="h-full w-full object-cover transition-transform duration-700 group-hover:scale-110" />
  453. ) : <div className="h-full w-full bg-slate-800" />}
  454. <div className="absolute inset-0 bg-gradient-to-t from-[#050b14] to-transparent opacity-60" />
  455. </div>
  456. <div className="p-6">
  457. <h3 className="line-clamp-1 text-[15px] font-bold text-white group-hover:text-[#f3deae] transition-colors">{c.title}</h3>
  458. </div>
  459. </Link>
  460. ))
  461. ) : (
  462. <div className="col-span-full py-12 flex items-center justify-center rounded-3xl border border-dashed border-white/10 bg-white/5 text-sm font-medium text-slate-500">
  463. 暂无学习中的课程
  464. </div>
  465. )}
  466. </div>
  467. </section>
  468. </div>
  469. </div>
  470. </div>
  471. {/* 黑金风格确认弹窗 */}
  472. {cancelTarget && (
  473. <div className="fixed inset-0 z-50 flex items-center justify-center bg-[#050b14]/80 p-4 backdrop-blur-md">
  474. <div className="w-full max-w-sm overflow-hidden rounded-[2.5rem] border border-white/10 bg-[#0a1120] shadow-2xl">
  475. <div className="p-10 text-center">
  476. <div className="mx-auto mb-6 flex h-20 w-20 items-center justify-center rounded-full bg-rose-500/10 text-rose-400">
  477. <FileText size={32} />
  478. </div>
  479. <h3 className="text-xl font-bold text-white">确认撤销订单?</h3>
  480. <p className="mt-3 text-sm leading-relaxed text-slate-400 px-4">
  481. 订单流水号 <span className="text-slate-200">{cancelTarget.serial}</span> 撤销后该操作不可恢复。
  482. </p>
  483. </div>
  484. <div className="flex border-t border-white/5">
  485. <button onClick={() => setCancelTarget(null)} className="flex-1 py-5 text-sm font-bold text-slate-400 hover:bg-white/5">暂不处理</button>
  486. <div className="w-px bg-white/5" />
  487. <button onClick={handleConfirmCancel} disabled={cancelLoading} className="flex-1 py-5 text-sm font-bold text-rose-400 hover:bg-rose-500/10 disabled:opacity-50">
  488. {cancelLoading ? "执行中..." : "确认撤销"}
  489. </button>
  490. </div>
  491. </div>
  492. </div>
  493. )}
  494. {/* 消费明细弹框 */}
  495. {balanceDetailOpen && (
  496. <div className="fixed inset-0 z-[55] flex items-center justify-center bg-[#050b14]/80 p-4 backdrop-blur-md">
  497. <div className="w-full max-w-2xl overflow-hidden rounded-[2.5rem] border border-white/10 bg-[#0a1120] shadow-2xl">
  498. <div className="flex items-center justify-between border-b border-white/5 p-6 md:px-8">
  499. <h3 className="text-xl font-bold text-white flex items-center gap-2">
  500. <Wallet className="text-[#b89458]" size={20} /> 消费明细
  501. </h3>
  502. <button
  503. onClick={() => setBalanceDetailOpen(false)}
  504. className="text-sm font-bold text-slate-500 hover:text-white"
  505. >
  506. 关闭
  507. </button>
  508. </div>
  509. <div className="p-6 md:p-8 max-h-[70vh] overflow-y-auto space-y-4 custom-scrollbar">
  510. {balanceDetailLoading ? (
  511. <div className="py-12 flex justify-center text-slate-500">
  512. 数据加载中...
  513. </div>
  514. ) : balanceDetailError ? (
  515. <div className="rounded-2xl border border-rose-500/20 bg-rose-500/10 p-4 text-sm text-rose-400">
  516. {balanceDetailError}
  517. </div>
  518. ) : balanceRecords.length === 0 ? (
  519. <div className="py-16 flex flex-col items-center justify-center rounded-3xl border border-dashed border-white/10 bg-white/5 text-slate-500">
  520. <p className="text-sm">暂无消费明细</p>
  521. </div>
  522. ) : (
  523. <div className="space-y-3">
  524. {balanceRecords.map((r) => (
  525. <div
  526. key={r.id}
  527. className="rounded-[1.5rem] border border-white/5 bg-white/5 p-5 transition-all hover:bg-white/10"
  528. >
  529. <div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
  530. <div>
  531. <p className="text-[15px] font-bold text-white">
  532. {balanceTypeLabel(r.type)}
  533. </p>
  534. <p className="mt-1 text-xs font-medium text-slate-500">
  535. 添加时间: {r.addTime || "-"}
  536. </p>
  537. </div>
  538. <div className="text-right">
  539. <p className="text-xl font-bold text-white tracking-tight">
  540. ${Number(r.amount ?? 0).toFixed(2)}
  541. </p>
  542. <span className="inline-block mt-1 rounded-full border border-white/10 bg-white/5 px-3 py-0.5 text-[10px] font-bold uppercase tracking-widest text-slate-400">
  543. type: {r.type}
  544. </span>
  545. </div>
  546. </div>
  547. </div>
  548. ))}
  549. </div>
  550. )}
  551. </div>
  552. </div>
  553. </div>
  554. )}
  555. {/* 修改信息弹框 */}
  556. {editInfoOpen && (
  557. <div className="fixed inset-0 z-[65] flex items-center justify-center bg-[#050b14]/80 p-4 backdrop-blur-md">
  558. <div className="w-full max-w-md overflow-hidden rounded-[2.5rem] border border-white/10 bg-[#0a1120] shadow-2xl">
  559. <div className="flex items-center justify-between border-b border-white/5 p-6 md:px-8">
  560. <h3 className="text-xl font-bold text-white flex items-center gap-2">
  561. <UserRoundPen className="text-[#b89458]" size={20} /> 修改信息
  562. </h3>
  563. <button
  564. type="button"
  565. onClick={closeEditInfo}
  566. disabled={editSubmitting}
  567. className="text-sm font-bold text-slate-500 hover:text-white disabled:opacity-50"
  568. >
  569. 关闭
  570. </button>
  571. </div>
  572. <div className="p-6 md:p-8 space-y-4">
  573. {editError ? (
  574. <div className="rounded-2xl border border-rose-500/20 bg-rose-500/10 p-4 text-sm text-rose-400">
  575. {editError}
  576. </div>
  577. ) : null}
  578. <div className="space-y-4">
  579. <div>
  580. <label className="text-sm font-bold text-slate-300">姓名</label>
  581. <input
  582. value={editName}
  583. onChange={(e) => setEditName(e.target.value)}
  584. className="mt-2 w-full rounded-xl border border-white/10 bg-black/20 px-4 py-3 text-sm text-white placeholder-slate-500 focus:border-[#b89458] focus:outline-none focus:ring-1 focus:ring-[#b89458]"
  585. placeholder="请输入姓名"
  586. />
  587. </div>
  588. <div>
  589. <label className="text-sm font-bold text-slate-300">手机号</label>
  590. <input
  591. value={editPhone}
  592. onChange={(e) => setEditPhone(e.target.value)}
  593. className="mt-2 w-full rounded-xl border border-white/10 bg-black/20 px-4 py-3 text-sm text-white placeholder-slate-500 focus:border-[#b89458] focus:outline-none focus:ring-1 focus:ring-[#b89458]"
  594. placeholder="请输入手机号"
  595. />
  596. </div>
  597. <div>
  598. <label className="text-sm font-bold text-slate-300">证件号</label>
  599. <input
  600. value={editIdentity}
  601. onChange={(e) => setEditIdentity(e.target.value)}
  602. className="mt-2 w-full rounded-xl border border-white/10 bg-black/20 px-4 py-3 text-sm text-white placeholder-slate-500 focus:border-[#b89458] focus:outline-none focus:ring-1 focus:ring-[#b89458]"
  603. placeholder="请输入证件号"
  604. />
  605. </div>
  606. </div>
  607. </div>
  608. <div className="flex gap-4 border-t border-white/5 p-6 bg-black/20">
  609. <button
  610. type="button"
  611. onClick={closeEditInfo}
  612. disabled={editSubmitting}
  613. className="flex-1 rounded-xl bg-white/10 py-4 font-bold text-white hover:bg-white/20 transition-all disabled:opacity-50"
  614. >
  615. 取消
  616. </button>
  617. <button
  618. type="button"
  619. onClick={submitEditInfo}
  620. disabled={editSubmitting}
  621. className="flex-1 rounded-xl bg-gradient-to-br from-[#f3deae] to-[#d9be88] py-4 font-bold text-[#5c461a] shadow-lg hover:opacity-90 transition-all disabled:opacity-50"
  622. >
  623. {editSubmitting ? "提交中..." : "确认修改"}
  624. </button>
  625. </div>
  626. </div>
  627. </div>
  628. )}
  629. {/* 退出登录确认弹框 */}
  630. {logoutConfirmOpen && (
  631. <div className="fixed inset-0 z-[80] flex items-center justify-center bg-[#050b14]/80 p-4 backdrop-blur-md">
  632. <div className="w-full max-w-sm overflow-hidden rounded-[2.5rem] border border-white/10 bg-[#0a1120] shadow-2xl">
  633. <div className="p-10 text-center">
  634. <div className="mx-auto mb-6 flex h-16 w-16 items-center justify-center rounded-full bg-rose-500/10 text-rose-400">
  635. <LogOut size={28} />
  636. </div>
  637. <h3 className="text-xl font-bold text-white">确认退出登录?</h3>
  638. <p className="mt-3 text-sm leading-relaxed text-slate-400 px-4">
  639. 退出后将清空本地缓存信息。
  640. </p>
  641. </div>
  642. <div className="flex border-t border-white/5">
  643. <button
  644. type="button"
  645. onClick={() => setLogoutConfirmOpen(false)}
  646. className="flex-1 py-5 text-sm font-bold text-slate-400 hover:bg-white/5"
  647. >
  648. 取消
  649. </button>
  650. <div className="w-px bg-white/5" />
  651. <button
  652. type="button"
  653. onClick={() => {
  654. setLogoutConfirmOpen(false);
  655. logout();
  656. if (typeof window !== "undefined") window.location.reload();
  657. }}
  658. className="flex-1 py-5 text-sm font-bold text-rose-400 hover:bg-rose-500/10"
  659. >
  660. 确认退出
  661. </button>
  662. </div>
  663. </div>
  664. </div>
  665. )}
  666. </div>
  667. );
  668. }